Skip to content

Conversation

@Fuzzwah
Copy link

@Fuzzwah Fuzzwah commented Jan 4, 2026

Add DML_ONLY Access Mode

Overview

Adds a new DML_ONLY access mode to postgres-mcp that allows data manipulation (INSERT, UPDATE, DELETE, SELECT) while blocking schema changes and other DDL operations. This provides a middle ground between UNRESTRICTED (allows everything) and RESTRICTED (read-only) modes.

Motivation

Users often need to perform data modifications without having the ability to alter database schema. The existing access modes didn't support this use case:

  • UNRESTRICTED: Too permissive, allows DDL operations
  • RESTRICTED: Too restrictive, blocks all writes including DML

DML_ONLY mode enables safe data manipulation for use cases like:

  • Application agents that need to insert/update data but shouldn't modify schema
  • Data import/migration scripts that should be isolated from structural changes
  • Development environments where schema changes require explicit approval

Implementation Details

New Components:

  • DmlOnlySqlDriver class in src/postgres_mcp/sql/dml_only_sql.py
    • Wraps underlying SqlDriver with validation layer
    • Uses pglast to parse and validate SQL AST before execution
    • Reuses SafeSqlDriver.ALLOWED_FUNCTIONS and extends ALLOWED_NODE_TYPES

Allowed Operations:

  • SELECT - Read queries
  • INSERT - Including UPSERT with ON CONFLICT
  • UPDATE - With all standard clauses (WHERE, RETURNING, etc.)
  • DELETE - With all standard clauses
  • EXPLAIN - Query planning (but not EXPLAIN ANALYZE)
  • SHOW - View configuration variables
  • ✅ Complex queries (CTEs, subqueries, JOINs, CASE expressions)

Blocked Operations:

  • CREATE/ALTER/DROP TABLE
  • CREATE/DROP INDEX
  • CREATE/DROP VIEW/FUNCTION/SCHEMA/DATABASE
  • TRUNCATE
  • VACUUM
  • CREATE/DROP EXTENSION
  • SET (configuration changes)
  • BEGIN/COMMIT/ROLLBACK (transaction control)
  • EXPLAIN ANALYZE (can impact performance)

Usage:

# Start server with DML_ONLY mode
mcp-server-postgres postgres://user:pass@localhost/dbname --access-mode dml_only

# With timeout (recommended)
mcp-server-postgres postgres://user:pass@localhost/dbname \
  --access-mode dml_only \
  --query-timeout 30

Testing

  • 48 unit tests for DML_ONLY driver covering:
    • 13 tests for allowed DML operations
    • 18 tests for blocked DDL operations
    • 6 tests for complex queries (CTEs, subqueries, etc.)
    • 9 tests for error handling and edge cases
    • 2 tests for WHERE clause requirement validation
  • 7 integration tests for access mode selection
  • All existing tests continue to pass (no regression)

Design Decisions

  1. RawStmt Validation: Validates inner statement (stmt.stmt) to properly check the actual SQL command, not just the wrapper node

  2. Function Allow-list: Reuses SafeSqlDriver.ALLOWED_FUNCTIONS to maintain consistency with read-only mode's security model

  3. Error Messages: Provides specific, actionable error messages (e.g., "Statement type CreateStmt not allowed in DML_ONLY mode") rather than generic failures

  4. Timeout Handling: Returns TimeoutError (not ValueError) for query timeouts, enabling callers to distinguish timeout from validation failures

  5. DELETE/UPDATE without WHERE: Required for safety. UPDATE and DELETE statements must include a WHERE clause to prevent accidental modification or deletion of all rows. This is a critical safety feature that helps prevent data loss from mistaken queries

Files Changed

  • src/postgres_mcp/server.py - Add DML_ONLY to AccessMode enum, update driver selection
  • src/postgres_mcp/sql/dml_only_sql.py - New DmlOnlySqlDriver implementation
  • src/postgres_mcp/sql/__init__.py - Export DmlOnlySqlDriver
  • tests/unit/sql/test_dml_only_sql.py - New comprehensive test suite
  • tests/unit/test_access_mode.py - Add DML_ONLY test cases
  • README.md - Document new access mode and usage

Related Documentation

Implementation follows the plan outlined in DML_ONLY_MODE_IMPLEMENTATION.md

Breaking Changes

None. This is a purely additive feature that doesn't modify existing behavior.

- Add DmlOnlySqlDriver class to allow DML operations while blocking DDL
- Support INSERT, UPDATE, DELETE, and UPSERT operations
- Add comprehensive test suite with 46 unit tests
- Update documentation and CLI help text
- Provides middle-ground security between unrestricted and restricted modes
Addressed all critical issues from code review:

1. RawStmt validation: Now validates inner statement (stmt.stmt) instead of wrapper
2. Error messages: Removed generic wrapper, now returns specific detailed errors
3. Function validation: Use isinstance(node, FuncCall) before accessing funcname
4. Timeout handling: Changed from ValueError to TimeoutError for better error distinction
5. Type safety: Added proper DefElem and FuncCall imports and type checking

- Mirror SafeSqlDriver implementation patterns exactly
- Improve error message clarity (e.g., 'Statement type CreateStmt not allowed in DML_ONLY mode')
- Update all 46 tests to match new error message patterns
- All tests passing (53/53)
- Code style checks passing (ruff)
Add critical safety feature to prevent accidental data loss:

- UPDATE and DELETE statements now require WHERE clause
- Prevents accidental modification/deletion of all rows
- Added 2 new tests for WHERE clause validation
- Updated test_update_with_case to include WHERE clause
- Updated PR description to reflect this design decision

All 55 tests passing (48 DML_ONLY + 7 access mode).
@Fuzzwah Fuzzwah marked this pull request as ready for review January 4, 2026 22:16
@jssmith
Copy link
Contributor

jssmith commented Jan 22, 2026

This is a good feature addressing a real gap - application agents that need to modify data but shouldn't touch schema. The implementation is well-done: good test coverage, thoughtful safety features like the WHERE clause requirement for UPDATE/DELETE, and reuse of the existing SafeSqlDriver.ALLOWED_FUNCTIONS whitelist.

I wanted to think through a couple of things to make sure we get the design right.

One concern is the naming

The name DML_ONLY could be interpreted as slightly inaccurate. We have:

  • DQL (Data Query Language): SELECT
  • DML (Data Manipulation Language): INSERT, UPDATE, DELETE
  • DDL (Data Definition Language): CREATE, ALTER, DROP

Since this mode allows both DQL and DML while blocking DDL, calling it DML_ONLY might suggest SELECT wouldn't work (since SELECT is DQL, not DML). This could cause additional problems if we decide to expand the modes supported in the future.

Some alternatives:

  • NO_DDL - describes what's blocked
  • DATA_ONLY - describes what's accessible (data operations, not schema)
  • SCHEMA_PROTECTED - emphasizes the protection

Background on the design

We originally went with only two modes and with names that do not reference the technical specifics because it is difficult to make precise guarantees about the behavior. For example, we can restrict DDL when entered directly in a query, but what if it happens in a stored procedure or in an extension?

The README describes the current design as deliberately choosing "two extremes" of the convenience/safety spectrum - UNRESTRICTED for dev environments, RESTRICTED for production. We designed these modes around use cases.

Next steps

I've opened #138 to discuss the broader access modes design. Let's continue the conversation there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants